package com.xlhj.boot.service.impl;

import com.xlhj.boot.service.RedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Circle;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @ClassName RedisServiceImpl
 * @Description redis业务接口实现类
 * @Author liucaijing
 * @Date 2020/10/9 23:32
 * @Version 1.0
 */
@Service
public class RedisServiceImpl implements RedisService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    @Override
    public void set(String key, String value, long times) {
        redisTemplate.opsForValue().set(key, value, times);
    }

    @Override
    public String get(String key) {
        String value = redisTemplate.opsForValue().get(key);
        return value;
    }

    @Override
    public Integer append(String key, String value) {
        return redisTemplate.opsForValue().append(key, value);
    }

    @Override
    public Long strlen(String key) {
        return redisTemplate.opsForValue().size(key);
    }

    @Override
    public Boolean setnx(String key, String value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
        //return redisTemplate.opsForValue().setIfAbsent(key, value, 3, TimeUnit.SECONDS);
    }

    @Override
    public Long incr(String key) {
        return redisTemplate.opsForValue().increment(key);
    }

    @Override
    public Long decr(String key) {
        return redisTemplate.opsForValue().decrement(key);
    }

    @Override
    public Long incrby(String key, long step) {
        return redisTemplate.opsForValue().increment(key, step);
    }

    @Override
    public Long decrby(String key, long step) {
        return redisTemplate.opsForValue().decrement(key, step);
    }

    @Override
    public void mset(Map<String, String> map) {
        redisTemplate.opsForValue().multiSet(map);
    }

    @Override
    public List<String> mget(List<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    @Override
    public Boolean msetnx(Map<String, String> map) {
        return redisTemplate.opsForValue().multiSetIfAbsent(map);
    }

    @Override
    public String getrange(String key, long start, long stop) {
        return redisTemplate.opsForValue().get(key, start, stop);
    }

    @Override
    public void setex(String key, int expire, String value) {
        redisTemplate.opsForValue().set(key, value, expire, TimeUnit.SECONDS);
    }

    @Override
    public String getset(String key, String value) {
        return redisTemplate.opsForValue().getAndSet(key, value);
    }

    @Override
    public Boolean expireKey(String key, long times, TimeUnit timeUnit) {
        Boolean expire = redisTemplate.expire(key, times, timeUnit);
        return expire;
    }

    @Override
    public Long ttl(String key) {
        return redisTemplate.getExpire(key);
        //return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    @Override
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    @Override
    public Boolean existsKey(String key) {
        Boolean hasKey = redisTemplate.hasKey(key);
        return hasKey;
    }

    @Override
    public DataType type(String key) {
        return redisTemplate.type(key);
    }

    @Override
    public Boolean delete(String key) {
        Boolean delete = redisTemplate.delete(key);
        return delete;
    }

    @Override
    public Boolean unlink(String key) {
        return redisTemplate.unlink(key);
    }

    @Override
    public Long getDistributedLock() {
        String uuid = UUID.randomUUID().toString();
        String lockKey = "lock:100";
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 3, TimeUnit.SECONDS);
        Long execute = -1L;
        if (lock) {
            //执行业务逻辑
            //todo
            //定义lua脚本
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.cal('del', KEYS[1]) else return 0 end";
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptText(script);
            redisScript.setResultType(Long.class);
            execute = redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
        } else {
            try {
                Thread.sleep(1000);
                getDistributedLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return execute;
    }

    @Override
    public Boolean SimpleRateLimiter(String key, long period, int maxCount) {
        key = "hist:" + key;
        long milli = Instant.now().toEpochMilli();
        redisTemplate.multi();
        redisTemplate.opsForZSet().add(key, String.valueOf(milli), Double.valueOf(milli));
        redisTemplate.opsForZSet().removeRangeByScore(key, 0, milli - period * 1000);
        Long count = redisTemplate.opsForZSet().zCard(key);
        redisTemplate.expire(key, period, TimeUnit.SECONDS);
        redisTemplate.exec();
        return count <= maxCount;
    }

    @Override
    public Boolean FunnelRateLimiter(String key, int capacity, float leakingRate) {
        Map<String, Funnel> funnels = new HashMap<>();
        Funnel funnel = funnels.get(key);
        if (funnel == null) {
            funnel = new Funnel(capacity, leakingRate);
            funnels.put(key, funnel);
        }
        return funnel.watering(1);
    }

    @Override
    public Long lpush(String key, String value) {
        Long push = redisTemplate.opsForList().leftPush(key, value);
        return push;
    }

    @Override
    public Long lpush(String key, String... value) {
        Long pushAll = redisTemplate.opsForList().leftPushAll(key, value);
        return pushAll;
    }

    @Override
    public Long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    @Override
    public Long rpush(String key, String... value) {
        return redisTemplate.opsForList().rightPushAll(key, value);
    }

    @Override
    public String lpop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    @Override
    public String rpop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    @Override
    public String rpoplpush(String key1, String key2) {
        return redisTemplate.opsForList().rightPopAndLeftPush(key1, key2);
    }

    @Override
    public List<String> lrange(String key, int start, int stop) {
        return redisTemplate.opsForList().range(key, start, stop);
    }

    @Override
    public String lindex(String key, int index) {
        return redisTemplate.opsForList().index(key, index);
    }

    @Override
    public Long llen(String key) {
        return redisTemplate.opsForList().size(key);
    }

    @Override
    public Long linsert(String key, String oldValue, String newValue) {
        return redisTemplate.opsForList().leftPush(key, oldValue, newValue);
    }

    @Override
    public Long lrem(String key, int num, String value) {
        return redisTemplate.opsForList().remove(key, num, value);
    }

    @Override
    public void lset(String key, int index, String value) {
        redisTemplate.opsForList().set(key, index, value);
    }

    @Override
    public Long sadd(String key, String... value) {
        return redisTemplate.opsForSet().add(key, value);
    }

    @Override
    public Set<String> smembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    @Override
    public Boolean sismember(String key, String value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    @Override
    public Long scard(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    @Override
    public Long srem(String key, String... value) {
        return redisTemplate.opsForSet().remove(key, value);
    }

    @Override
    public String spop(String key) {
        return redisTemplate.opsForSet().pop(key);
    }

    @Override
    public List<String> srandmember(String key, int num) {
        return redisTemplate.opsForSet().randomMembers(key, num);
    }

    @Override
    public Boolean smove(String sourceKey, String value, String destKey) {
        return redisTemplate.opsForSet().move(sourceKey, value, destKey);
    }

    @Override
    public Set<String> sinter(String key1, String key2) {
        return redisTemplate.opsForSet().intersect(key1, key2);
    }

    @Override
    public Set<String> sunion(String key1, String key2) {
        return redisTemplate.opsForSet().union(key1, key2);
    }

    @Override
    public Set<String> sdiff(String key1, String key2) {
        return redisTemplate.opsForSet().difference(key1, key2);
    }

    @Override
    public void hset(String key, String field, String value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    @Override
    public Object hget(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    @Override
    public void hmset(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    @Override
    public Boolean hexists(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    @Override
    public Set<Object> hkeys(String key) {
        return redisTemplate.opsForHash().keys(key);
    }

    @Override
    public List<Object> hvals(String key) {
        return redisTemplate.opsForHash().values(key);
    }

    @Override
    public Long hincrby(String key, String field, int increment) {
        return redisTemplate.opsForHash().increment(key, field, increment);
    }

    @Override
    public Boolean hsetnx(String key, String field, String value) {
        return redisTemplate.opsForHash().putIfAbsent(key, field, value);
    }

    @Override
    public Boolean zadd(String key, String value, double score) {
        return redisTemplate.opsForZSet().add(key, value, score);
    }

    @Override
    public Set<String> zrange(String key, long start, long stop) {
        return redisTemplate.opsForZSet().range(key, start, stop);
    }

    @Override
    public Set<ZSetOperations.TypedTuple<String>> zrangewithscore(String key, long start, long stop) {
        return redisTemplate.opsForZSet().rangeWithScores(key, start, stop);
    }

    @Override
    public Set<String> zrangebyscore(String key, double min, double max) {
        return redisTemplate.opsForZSet().rangeByScore(key, min, max);
        //return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
    }

    @Override
    public Set<String> zrevrangebyscore(String key, double min, double max) {
        return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
        //return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, min, max);
    }

    @Override
    public Double zincrby(String key, String value, int increment) {
        return redisTemplate.opsForZSet().incrementScore(key, value, increment);
    }

    @Override
    public Long zrem(String key, String value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

    @Override
    public Long zcount(String key, double min, double max) {
        return redisTemplate.opsForZSet().count(key, min, max);
    }

    @Override
    public Long zrank(String key, String value) {
        return redisTemplate.opsForZSet().rank(key, value);
    }

    @Override
    public boolean setbit(String key, long offset, boolean value) {
        return redisTemplate.opsForValue().setBit(key, offset, value).booleanValue();
    }

    @Override
    public Boolean getbit(String key, long offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    @Override
    public Long bigcount(String key, long start, long end) {
        return redisTemplate.execute((RedisCallback<Long>) count -> count.bitCount(key.getBytes(), start, end));
        //return redisTemplate.execute((RedisCallback<Long>) count -> count.bitCount(key.getBytes()));
    }

    @Override
    public void bitop(RedisStringCommands.BitOperation operation, String destKey, String... keys) {
        redisTemplate.execute((RedisCallback<Long>) top -> top.bitOp(operation, destKey.getBytes(), Arrays.stream(keys).map(key -> key.getBytes()).collect(Collectors.toList()).toArray(new byte[keys.length][])));
    }

    @Override
    public Long pfadd(String key, String... value) {
        return redisTemplate.opsForHyperLogLog().add(key, value);
    }

    @Override
    public Long pfcount(String... keys) {
        return redisTemplate.opsForHyperLogLog().size(keys);
    }

    @Override
    public Long pfmerge(String destKey, String... sourceKeys) {
        return redisTemplate.opsForHyperLogLog().union(destKey, sourceKeys);
    }

    @Override
    public Long geoadd(String key, double longitude, double latitude, String member) {
        Point point = new Point(longitude, longitude);
        return redisTemplate.opsForGeo().add(key, point, member);
    }

    @Override
    public List<Point> geopos(String key, String... members) {
        return redisTemplate.opsForGeo().position(key, members);
    }

    @Override
    public Distance geodist(String key, String member1, String member2) {
        return redisTemplate.opsForGeo().distance(key, member1, member2);
        //return redisTemplate.opsForGeo().distance(key, member1, member2, Metrics.KILOMETERS);
    }

    @Override
    public void georadius(String key, double longitude, double latitude, double radius) {
        Point point = new Point(longitude, latitude);
        Circle circle = new Circle(point, radius);
        redisTemplate.opsForGeo().radius(key, circle);
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
        redisTemplate.opsForGeo().radius(key, circle, args);
    }

    @Override
    public void georadius(String key, String member, double radius) {
        redisTemplate.opsForGeo().radius(key, member, radius);
        Distance distance = new Distance(radius);
        redisTemplate.opsForGeo().radius(key, member, distance);
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().limit(2).sortAscending();
        redisTemplate.opsForGeo().radius(key, member, distance, args);
    }

    private class Funnel {
        int capacity;
        float leakingRate;
        int leftQuota;
        long leakingTs;

        public Funnel(int capacity, float leakingRate) {
            this.capacity = capacity;
            this.leakingRate = leakingRate;
            this.leftQuota = capacity;
            this.leakingTs = Instant.now().toEpochMilli();
        }

        void makeSpace() {
            long nowTs = Instant.now().toEpochMilli();
            long deltaTs = nowTs - leakingTs;
            int deltaQuota = (int) (deltaTs * leakingRate);
            if (deltaQuota < 0) {
                this.leftQuota = capacity;
                this.leakingTs = nowTs;
                return;
            }
            if (deltaQuota < 1) {
                return;
            }
            this.leftQuota += deltaQuota;
            this.leakingTs = nowTs;
            if (this.leftQuota > this.capacity) {
                this.leftQuota = this.capacity;
            }
        }

        boolean watering(int quota) {
            makeSpace();
            if (this.leftQuota >= quota) {
                this.leftQuota -= quota;
                return true;
            }
            return false;
        }
    }
}
